package controller

import (
	"context"
	"encoding/binary"

	"github.com/adwindone/sdk-go/crypto"
	"github.com/adwindone/sdk-go/model/blockchain"
	"github.com/adwindone/sdk-go/model/common"
	"github.com/adwindone/sdk-go/model/controller"
	"github.com/adwindone/sdk-go/model/request"
	"github.com/adwindone/sdk-go/model/response"
	"github.com/adwindone/sdk-go/util/hexutil"

	"github.com/google/uuid"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
)

// peopleChainControllerManage provides methods to interact with the blockchain controller service.

/*
GetBlockNumber returns the current block height. If flag is true, returns the Pending block height; if false, returns the latest block height
*/
func (pc *peopleChainControllerManage) GetBlockNumber(ctx context.Context, flag bool) (uint64, error) {
	info := new(controller.Flag)
	info.Flag = flag
	blockNumberInfo, err := pc.controllerService.GetBlockNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetBlockNumber", zap.Error(err))
		return 0, err
	}
	if blockNumberInfo == nil {
		pc.log.Error("GetBlockNumber", zap.String("msg", "block number info is null"))
		return 0, nil
	}
	return blockNumberInfo.BlockNumber, nil
}

/*
GetBlockHash returns the block hash at the specified block height

uint64 block_number: specified block height
*/
func (pc *peopleChainControllerManage) GetBlockHash(ctx context.Context, blockNumber uint64) (string, error) {
	info := new(controller.BlockNumber)
	info.BlockNumber = blockNumber
	hashInfo, err := pc.controllerService.GetBlockHash(ctx, info)
	if err != nil {
		pc.log.Error("GetBlockHash", zap.Error(err))
		return "", err
	}
	if hashInfo == nil {
		pc.log.Error("GetBlockHash", zap.String("msg", "hash info is null"))
		return "", nil
	}
	return hexutil.Encode(hashInfo.Hash), nil
}

/*
GetNodeStatus returns the current node status. Not supported in controller v6.3.4; supported in higher versions such as v6.7.2
*/
func (pc *peopleChainControllerManage) GetNodeStatus(ctx context.Context) (*response.NodeStatus, error) {
	nodeStatus, err := pc.controllerService.GetNodeStatus(ctx, &common.Empty{})
	if err != nil {
		pc.log.Error("GetNodeStatus", zap.Error(err))
		return nil, err
	}
	if nodeStatus == nil {
		pc.log.Error("GetNodeStatus", zap.String("msg", "node status info is null"))
		return nil, nil
	}
	retInfo, err := response.NodeStatusConvert(nodeStatus)
	if err != nil {
		pc.log.Error("GetNodeStatus", zap.String("msg", "node status convert failed"), zap.Error(err))
		return nil, err
	}
	return retInfo, nil
}

// SendRawTransaction sends a transaction to the blockchain
func (pc *peopleChainControllerManage) SendRawTransaction(ctx context.Context, rtx *request.Transaction) (string, error) {
	// Transaction structure
	tx := new(blockchain.Transaction)
	// nonce random number to prevent replay attacks
	tx.Nonce = uuid.NewString()
	// to recipient address
	tx.To = []byte(rtx.To)
	if len(rtx.To) > 0 {
		to, err := hexutil.Decode(rtx.To)
		if err != nil {
			pc.log.Error("to address decode", zap.Error(err))
			return "", err
		}
		tx.To = to
	}
	// Get current block height
	blockNumber, err := pc.GetBlockNumber(ctx, false)
	if err != nil {
		pc.log.Error("get block number", zap.Error(err))
		return "", err
	}
	// data transaction data
	data, err := hexutil.Decode(rtx.Data)
	if err != nil {
		pc.log.Error("data decode", zap.Error(err))
		return "", err
	}
	tx.Data = data
	// quota fee paid to miners
	tx.Quota = rtx.Quota
	// version contract version information
	tx.Version = rtx.Version
	// chainId chain identifier
	chainId, err := hexutil.Decode(rtx.ChainId)
	if err != nil {
		pc.log.Error("chainId decode", zap.Error(err))
		return "", err
	}
	tx.ChainId = chainId
	// value amount of native token in transaction
	valueBytes := make([]byte, 32)
	binary.BigEndian.PutUint32(valueBytes, rtx.Value)
	tx.Value = valueBytes
	// validUntilBlock timeout mechanism, transaction cancelled after specified block height
	tx.ValidUntilBlock = blockNumber + 99
	// transaction hash
	byteData, err := proto.Marshal(tx)
	if err != nil {
		pc.log.Error("tx proto marshal", zap.Error(err))
		return "", err
	}
	txHash := crypto.Sm3Hash(byteData)
	// sender address
	sender, err := hexutil.Decode(rtx.SenderAddress)
	if err != nil {
		pc.log.Error("sender address decode", zap.Error(err))
		return "", err
	}
	// signature
	signature, err := crypto.Signature(rtx.SenderPrivateKey, txHash)
	if err != nil {
		pc.log.Error("tx signature", zap.Error(err))
		return "", err
	}
	// transaction witness
	witness := new(blockchain.Witness)
	witness.Sender = sender
	witness.Signature = signature
	// transaction structure
	utx := new(blockchain.UnverifiedTransaction)
	utx.Transaction = tx
	utx.TransactionHash = txHash
	utx.Witness = witness
	// send transaction
	txRetInfo, err := pc.controllerService.SendRawTransaction(ctx, &blockchain.RawTransaction{Tx: &blockchain.RawTransaction_NormalTx{NormalTx: utx}})
	if err != nil {
		pc.log.Error("SendRawTransaction", zap.Error(err))
		return "", err
	}
	if txRetInfo == nil {
		pc.log.Error("SendRawTransaction", zap.String("msg", "transaction info is null"))
		return "", nil
	}
	return hexutil.Encode(txRetInfo.Hash), nil
}

// SendRawTransactions sends multiple transactions to the blockchain
func (pc *peopleChainControllerManage) SendRawTransactions(ctx context.Context, rtxs []*request.Transaction) ([]string, error) {
	normalTxs := make([]*blockchain.RawTransaction, 0)
	for _, rtx := range rtxs {
		// Transaction structure
		tx := new(blockchain.Transaction)
		// nonce random number to prevent replay attacks
		tx.Nonce = uuid.NewString()
		// to recipient address
		tx.To = []byte(rtx.To)
		if len(rtx.To) > 0 {
			to, err := hexutil.Decode(rtx.To)
			if err != nil {
				pc.log.Error("to address decode", zap.Error(err))
				return nil, err
			}
			tx.To = to
		}
		// Get current block height
		blockNumber, err := pc.GetBlockNumber(ctx, false)
		if err != nil {
			pc.log.Error("get block number", zap.Error(err))
			return nil, err
		}
		// data transaction data
		data, err := hexutil.Decode(rtx.Data)
		if err != nil {
			pc.log.Error("contract code decode", zap.Error(err))
			return nil, err
		}
		tx.Data = data
		// quota fee paid to miners
		tx.Quota = rtx.Quota
		// version contract version information
		tx.Version = rtx.Version
		// chainId chain identifier
		chainId, err := hexutil.Decode(rtx.ChainId)
		if err != nil {
			pc.log.Error("chainId decode", zap.Error(err))
			return nil, err
		}
		tx.ChainId = chainId
		// value amount of native token in transaction
		valueBytes := make([]byte, 32)
		binary.BigEndian.PutUint32(valueBytes, rtx.Value)
		tx.Value = valueBytes
		// validUntilBlock timeout mechanism, transaction cancelled after specified block height
		tx.ValidUntilBlock = blockNumber + 99
		// transaction hash
		byteData, err := proto.Marshal(tx)
		if err != nil {
			pc.log.Error("tx proto marshal", zap.Error(err))
			return nil, err
		}
		txHash := crypto.Sm3Hash(byteData)
		// sender address
		sender, err := hexutil.Decode(rtx.SenderAddress)
		if err != nil {
			pc.log.Error("sender address decode", zap.Error(err))
			return nil, err
		}
		// signature
		signature, err := crypto.Signature(rtx.SenderPrivateKey, txHash)
		if err != nil {
			pc.log.Error("tx signature", zap.Error(err))
			return nil, err
		}
		// transaction witness
		witness := new(blockchain.Witness)
		witness.Sender = sender
		witness.Signature = signature
		// transaction structure
		utx := new(blockchain.UnverifiedTransaction)
		utx.Transaction = tx
		utx.TransactionHash = txHash
		utx.Witness = witness
		normalTxs = append(normalTxs, &blockchain.RawTransaction{Tx: &blockchain.RawTransaction_NormalTx{NormalTx: utx}})
	}
	// send transaction
	txRetList, err := pc.controllerService.SendRawTransactions(ctx, &blockchain.RawTransactions{Body: normalTxs})
	if err != nil {
		pc.log.Error("SendRawTransactions", zap.Error(err))
		return nil, err
	}
	hashList := make([]string, 0)
	for _, txRetInfo := range txRetList.Hashes {
		hashList = append(hashList, hexutil.Encode(txRetInfo.Hash))
	}
	return hashList, nil
}

// GetTransaction returns transaction information by transaction hash
func (pc *peopleChainControllerManage) GetTransaction(ctx context.Context, txHash string) (*response.RawTransaction, error) {
	hash, err := hexutil.Decode(txHash)
	if err != nil {
		return nil, err
	}
	info := new(common.Hash)
	info.Hash = hash
	txInfo, err := pc.controllerService.GetTransaction(ctx, info)
	if err != nil {
		pc.log.Error("GetTransaction", zap.Error(err))
		return nil, err
	}
	if txInfo == nil {
		pc.log.Error("GetTransaction", zap.String("msg", "transaction info is null"))
		return nil, nil
	}
	newTxInfo := response.RawTransactionConvert(txInfo)
	return newTxInfo, nil
}

// GetTransactionBlockNumber returns the block height by transaction hash
func (pc *peopleChainControllerManage) GetTransactionBlockNumber(ctx context.Context, txHash string) (uint64, error) {
	hash, err := hexutil.Decode(txHash)
	if err != nil {
		return 0, err
	}
	info := new(common.Hash)
	info.Hash = hash
	blockNumberInfo, err := pc.controllerService.GetTransactionBlockNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetTransactionBlockNumber", zap.Error(err))
		return 0, err
	}
	if blockNumberInfo == nil {
		pc.log.Error("GetTransactionBlockNumber", zap.String("msg", "block number info is null"))
		return 0, nil
	}
	return blockNumberInfo.BlockNumber, nil
}

// GetTransactionIndex returns the transaction index in block by transaction hash
func (pc *peopleChainControllerManage) GetTransactionIndex(ctx context.Context, txHash string) (uint64, error) {
	hash, err := hexutil.Decode(txHash)
	if err != nil {
		return 0, err
	}
	info := new(common.Hash)
	info.Hash = hash
	txIndexInfo, err := pc.controllerService.GetTransactionIndex(ctx, info)
	if err != nil {
		pc.log.Error("GetTransactionIndex", zap.Error(err))
		return 0, err
	}
	if txIndexInfo == nil {
		pc.log.Error("GetTransactionIndex", zap.String("msg", "transaction index info is null"))
		return 0, nil
	}
	return txIndexInfo.TxIndex, nil
}

// GetBlockByHash returns block information by block hash
func (pc *peopleChainControllerManage) GetBlockByHash(ctx context.Context, blockHash string) (*blockchain.CompactBlock, error) {
	hash, err := hexutil.Decode(blockHash)
	if err != nil {
		return nil, err
	}
	info := new(common.Hash)
	info.Hash = hash
	blockInfo, err := pc.controllerService.GetBlockByHash(ctx, info)
	if err != nil {
		pc.log.Error("GetBlockByHash", zap.Error(err))
		return nil, err
	}
	return blockInfo, nil
}

// GetHeightByHash returns block height by block hash
func (pc *peopleChainControllerManage) GetHeightByHash(ctx context.Context, blockHash string) (uint64, error) {
	hash, err := hexutil.Decode(blockHash)
	if err != nil {
		return 0, err
	}
	info := new(common.Hash)
	info.Hash = hash
	heightInfo, err := pc.controllerService.GetHeightByHash(ctx, info)
	if err != nil {
		pc.log.Error("GetHeightByHash", zap.Error(err))
		return 0, err
	}
	if heightInfo == nil {
		pc.log.Error("GetBlockNumber", zap.String("msg", "block number info is null"))
		return 0, nil
	}
	return heightInfo.BlockNumber, nil
}

// GetBlockByNumber returns block information by block height
func (pc *peopleChainControllerManage) GetBlockByNumber(ctx context.Context, blockNumber uint64) (*blockchain.CompactBlock, error) {
	info := new(controller.BlockNumber)
	info.BlockNumber = blockNumber
	blockInfo, err := pc.controllerService.GetBlockByNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetBlockByNumber", zap.Error(err))
		return nil, err
	}
	return blockInfo, nil
}

// GetBlockDetailByNumber returns block detail information (including transaction information) by block height
func (pc *peopleChainControllerManage) GetBlockDetailByNumber(ctx context.Context, blockNumber uint64) (*blockchain.Block, error) {
	info := new(controller.BlockNumber)
	info.BlockNumber = blockNumber
	blockInfo, err := pc.controllerService.GetBlockDetailByNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetBlockDetailByNumber", zap.Error(err))
		return nil, err
	}
	return blockInfo, nil
}

// GetStateRootByNumber returns the StateRoot (root hash value of state tree) by block height
func (pc *peopleChainControllerManage) GetStateRootByNumber(ctx context.Context, blockNumber uint64) (*common.StateRoot, error) {
	info := new(controller.BlockNumber)
	info.BlockNumber = blockNumber
	stateRootInfo, err := pc.controllerService.GetStateRootByNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetStateRootByNumber", zap.Error(err))
		return nil, err
	}
	return stateRootInfo, nil
}

// GetProofByNumber returns the Proof (block legal proof) by block height
func (pc *peopleChainControllerManage) GetProofByNumber(ctx context.Context, blockNumber uint64) (*common.Proof, error) {
	info := new(controller.BlockNumber)
	info.BlockNumber = blockNumber
	proofInfo, err := pc.controllerService.GetProofByNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetProofByNumber", zap.Error(err))
		return nil, err
	}
	return proofInfo, nil
}

// GetSystemConfig returns the current system configuration
func (pc *peopleChainControllerManage) GetSystemConfig(ctx context.Context) (*controller.SystemConfig, error) {
	systemConfig, err := pc.controllerService.GetSystemConfig(ctx, &common.Empty{})
	if err != nil {
		pc.log.Error("GetSystemConfig", zap.Error(err))
		return nil, err
	}
	return systemConfig, nil
}

// GetSystemConfigByNumber returns the system configuration by block height
func (pc *peopleChainControllerManage) GetSystemConfigByNumber(ctx context.Context, blockNumber uint64) (*controller.SystemConfig, error) {
	info := new(controller.BlockNumber)
	info.BlockNumber = blockNumber
	systemConfig, err := pc.controllerService.GetSystemConfigByNumber(ctx, info)
	if err != nil {
		pc.log.Error("GetSystemConfigByNumber", zap.Error(err))
		return nil, err
	}
	return systemConfig, nil
}

// AddNode adds a node to the blockchain network
func (pc *peopleChainControllerManage) AddNode(ctx context.Context, in *common.NodeNetInfo) (*common.StatusCode, error) {
	statusCode, err := pc.controllerService.AddNode(ctx, in)
	if err != nil {
		pc.log.Error("AddNode", zap.Error(err))
		return nil, err
	}
	return statusCode, nil
}

// GetCrossChainProof returns the cross-chain proof by transaction hash
func (pc *peopleChainControllerManage) GetCrossChainProof(ctx context.Context, txHash string) (*controller.CrossChainProof, error) {
	hash, err := hexutil.Decode(txHash)
	if err != nil {
		return nil, err
	}
	info := new(common.Hash)
	info.Hash = hash
	crossChainProof, err := pc.controllerService.GetCrossChainProof(ctx, info)
	if err != nil {
		pc.log.Error("GetCrossChainProof", zap.Error(err))
		return nil, err
	}
	return crossChainProof, nil
}
